#pragma once
#include <osg/Geode>
#include <osg/Geometry>
#include <osg/Program>
#include <osg/Shader>
#include <osgDB/ReadFile>
#include <osgDB/FileUtils>
#include <osg/PositionAttitudeTransform>
#include "InstancedDrawCallback.h"
#include "PbrMaterial.h"
#include "StateSet.h"
#include "Light.h"

class Entity : public osg::PositionAttitudeTransform {
public:
	Entity(Entity* parent = nullptr) {
		m_geometry = new osg::Geometry;
		m_stateSet = m_geometry->getOrCreateStateSet();
		osg::PositionAttitudeTransform::addChild(m_geometry);
		m_uniform = OUniform(m_stateSet);

		setParent(parent);
	}

	void setParent(Entity* parent) {
		if (!parent) return;
		parent->addChild(this);
	}

	void install(const CString& shaderName) {
		bool isVerticesValid = m_vertices != nullptr && m_vertices->getNumElements() > 0;
		if (!isVerticesValid) {
			LogW << "m_vertices is empty! (shader:'" << shaderName << "')";
			return;
		}

		createProgram(shaderName);

		setVertexArray(GLSLVarName::Vertex, m_vertices);
		setVertexArray(GLSLVarName::Normal, m_normals);
		setVertexArray(GLSLVarName::Texcoord, m_texcoords);
		setVertexArray(GLSLVarName::InstancedVertex, m_instancedVertices);

		int numInstances = m_instancedVertices == nullptr ? 0 : m_instancedVertices->getNumElements();
		if (m_indices.empty())
			m_geometry->addPrimitiveSet(new osg::DrawArrays(m_primitiveType, 0, m_vertices->size(), numInstances));
		else
			m_geometry->addPrimitiveSet(new osg::DrawElementsUInt(m_primitiveType, m_indices.size(), m_indices.data(), numInstances));

		if (m_hasModelMatrix)
			m_uniform.add(GLSLVarName::M, m_modelMatrix);

		m_stateSet->setAttribute(m_program);
	}

	void setPrimitiveType(PrimitiveType primitiveType) {
		m_primitiveType = primitiveType;
		// May managed by OSG auto
		//if (osg::PrimitiveSet::TRIANGLE_STRIP == primitiveMode || osg::PrimitiveSet::TRIANGLE_FAN == primitiveMode)
		//	StateSet::setCullingFace(this, false);
	}

	void setVertices(const CVector<P>& vertices, float y, bool isInstancedVertex = false) {
		auto _vertices = OUtil::toVec3Array(vertices, y);
		if (isInstancedVertex) m_instancedVertices = _vertices;
		else m_vertices = _vertices;
	}
	void setVertices(const CVector<P3D>& vertices, bool isInstancedVertex = false) {
		auto _vertices = OUtil::toVec3Array(vertices);
		if (isInstancedVertex) m_instancedVertices = _vertices;
		else m_vertices = _vertices;
	}

	void setIndices(const CVector<UInt>& indices) { m_indices = indices; }

	void setNormals(const CVector<P3D>& normals) { m_normals = OUtil::toVec3Array(normals); }

	void setTexcoords(const CVector<P2D>& texcoords) { m_texcoords = OUtil::toVec2Array(texcoords); }

	void setModelMatrix(const Mat& modelMatrix) {
		return;
		m_modelMatrix = modelMatrix;
		m_hasModelMatrix = true;
	}

	void setColor(const CColor& color) { addUniform("color", color); }

	void setLightEnabled(bool enabled, Light::Type type = Light::Type::Directional) {
		addUniform("enableLight", enabled);
		if (!enabled)
			return;
		addUniform("pbrGlobalAmbientCompnt", Config::Material_PBRGlobalAmbientCompnt);
		Switch status = enabled ? Switch::On : Switch::Off;
		switch (type) {
			case Light::Type::Directional: Light::Directional::get()->setUniform(m_uniform, status); break;
			case Light::Type::Point: Light::Point::get()->setUniform(m_uniform, status); break;
			case Light::Type::Spot: Light::Spot::get()->setUniform(m_uniform, status); break;
			default: break;
		}
	}

	void setCullingFaceEnabled(bool enabled) { StateSet::setCullingFace(this, enabled); }
	void enableTransparency() { StateSet::enableBlending(this); }

	bool isVisible() const { return this->getNodeMask() != 0x0; }
	void setVisible(bool visible) { this->setNodeMask(visible ? 0xFFFFFFFF : 0x0); }

	void setFadeOutEdgeEnabled(float customEndZ = InvalidNum, float customZOffset = InvalidNum) {
		addUniformStruct("view3d",
			Field("ssaa", 1.0),//SSAA
			Field("w", W3D),
			Field("h", H3D),
			Field("fadeOffsetHorizonPx", Config::View3D_FadeOffsetHorizonPx),
			Field("fadeOffsetSkylinePx", Config::View3D_FadeOffsetSkylinePx),
			Field("skylineFromTopPx", Config::View3D_SkylineFromTopPx)
		);
		if (customEndZ != InvalidNum && customZOffset != InvalidNum) {
			addUniform("fadeEndZ", customEndZ);
			addUniform("fadeZOffset", customZOffset);
		}
	}

	void setMeshProperty(int meshIndex, const CString& meshName) { m_meshProperty = MakePair(meshIndex, meshName); }
	const Pair<int, CString>& meshProperty() const { return m_meshProperty; }

	void setPosition(const P& pos, float y) { osg::PositionAttitudeTransform::setPosition(osg::Vec3(pos.x, y, pos.z)); }
	void setPosition(const P3D& pos) { osg::PositionAttitudeTransform::setPosition(osg::Vec3(pos.x, pos.y, pos.z)); }
	void setRotation(float angle, const P3D& axis = Axis_Y) { osg::PositionAttitudeTransform::setAttitude(osg::Quat(angle, OUtil::toVec3(axis))); }// Unit:radians
	void setScale(float scale) { osg::PositionAttitudeTransform::setScale(osg::Vec3(scale, scale, scale)); }
	void setScale(float sx, float sy, float sz) { osg::PositionAttitudeTransform::setScale(osg::Vec3(sx, sy, sz)); }
	void rotateAround(const P3D& worldCenter, float angle) {// unit: radians
		setPosition(worldCenter);// (a)translate to the center of rotation
		setRotation(angle);// (b)rotate around the local origin
		setPosition(OUtil::fromVec3(osg::PositionAttitudeTransform::getPosition()) - worldCenter);// (c)reverse translation
	}

	void setMaterial(const PbrMaterial& pbrMat) {
		addUniformStruct("material",
			Field("baseColor", pbrMat.baseColor),
			Field("metallic", pbrMat.metallic),
			Field("roughness", pbrMat.roughness)
		);
		if (pbrMat.texture) {
			GLuint textureUnit = 1;// GL_TEXTURE0 May should reserve for default
			addUniform("isTexImage", true);
			addUniform("textureImg", textureUnit, pbrMat.texture);
		}
	}

	void setMaterialColor(const CColor& color) { addUniform("material.baseColor", color); }

	void mixBackground(osg::Texture2D* bgTexture) {
		addUniform("bgTexture", 0, bgTexture);// use default texture unit: GL_TEXTURE0
	}

	template <typename T>
	void setExtraVertexArray(const std::string& name, const CVector<T>& attribData) {
		osg::Array* array = nullptr;
		if constexpr (std::is_same_v<T, int>)
			array = OUtil::toIntArray(attribData);
		else if constexpr (std::is_same_v<T, float> || std::is_same_v<T, double>)
			array = OUtil::toFloatArray(attribData);
		else if constexpr (std::is_same_v<T, P> || std::is_same_v<T, P2D>)
			array = OUtil::toVec2Array(attribData);
		else if constexpr (std::is_same_v<T, P3D>)
			array = OUtil::toVec3Array(attribData);
		else
			EXIT("T is unknown type");
		setVertexArray(name, array);
	}

	void addChild(Entity* entity) {
		osg::PositionAttitudeTransform::addChild(entity);// into OSG hierarchy for rendering reachable
		m_children << entity;// records direct Entity children for convenience
	}
	CVector<Entity*> children() { return m_children; }

	/************* Uniform About ***************/
	template<typename T>
	void addUniform(const std::string& name, const T& value, osg::Texture2D* texture = nullptr) { m_uniform.add(name, value, texture); }

	template<typename T, typename... Args>
	void addUniform(const std::string& name, T value, const std::string& nextName, Args... args) { m_uniform.add(name, value, nextName, args...); }

	template<typename T, typename... Args>
	void addUniformStruct(const std::string& structName, const Pair<const char*, T>& field, Args... args) { m_uniform.addStruct(structName, field, args...); }

	template<typename T>
	void addUniformArray(const std::string& name, const CVector<T>& values) { m_uniform.addArray(name, values); }

	virtual void update() {}

protected:
	/**
	 *@note use `protected` modifier to avoid creating objects on the stack, so you must use `new` to create object!
	 *@note use `virtual` modifier to make sure derived class's destructor be called, otherwise, when `delete` base pointer, only base destructor be called!
	 */
	virtual ~Entity() {
		//std::cout << ">>> ~Entity left: " << --N << std::endl;
	}

private:
	void createProgram(const CString& shaderName) {
		static CMap<CString, osg::ref_ptr<osg::Program>> ShaderProgramMap;
		if (ShaderProgramMap.contains(shaderName))
			m_program = ShaderProgramMap[shaderName];
		else {
			m_program = new osg::Program;
			auto vShader = osgDB::readShaderFile(ShaderType::VERTEX, Config::shaderPath(ShaderType::VERTEX, shaderName));
			auto fShader = osgDB::readShaderFile(ShaderType::FRAGMENT, Config::shaderPath(ShaderType::FRAGMENT, shaderName));
			m_program->addShader(vShader);
			m_program->addShader(fShader);
			auto gShaderPath = Config::shaderPath(ShaderType::GEOMETRY, shaderName);
			if (osgDB::fileExists(gShaderPath)) {
				auto gShader = osgDB::readShaderFile(ShaderType::GEOMETRY, gShaderPath);
				m_program->addShader(gShader);
			}
			ShaderProgramMap.insert(shaderName, m_program);
		}
	}

	void setVertexArray(GLSLVarName attribNameEnum, osg::Array* vertexArray) {
		setVertexArray(Enum_toStr(attribNameEnum), vertexArray);
	}

	void setVertexArray(const std::string& attribName, osg::Array* vertexArray) {
		if (!vertexArray || vertexArray->getNumElements() == 0)
			return;
		++m_vboIndex;
		m_geometry->setVertexAttribArray(m_vboIndex, vertexArray, osg::Array::BIND_PER_VERTEX);
		m_program->addBindAttribLocation(attribName, m_vboIndex);

		if (Enum_toStr(GLSLVarName::InstancedVertex) == attribName)
			m_geometry->setDrawCallback(new InstancedDrawCallback(m_vboIndex));
	}

	osg::Geometry* m_geometry = nullptr;

	osg::StateSet* m_stateSet = nullptr;

	osg::Program* m_program = nullptr;

	int m_vboIndex = -1;

	PrimitiveType m_primitiveType = PrimitiveType::TRIANGLES;

	osg::Vec3Array* m_vertices = nullptr, *m_instancedVertices = nullptr;
	CVector<GLuint> m_indices;
	osg::Vec3Array* m_normals = nullptr;
	osg::Vec2Array* m_texcoords = nullptr;

	Mat m_modelMatrix;
	bool m_hasModelMatrix = false;

	Pair<int, CString> m_meshProperty;// (meshIndex, meshName)

	OUniform m_uniform;
	
	CVector<Entity*> m_children;
};